#include "execplugin.h"

#include <string.h>
#include <stdio.h>
#include <cairo.h>
#include <cairo-xlib.h>
#include <math.h>
#include <pango/pangocairo.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <time.h>

#include "window.h"
#include "server.h"
#include "panel.h"
#include "timer.h"
#include "common.h"

PangoFontDescription *execp_font_desc;
int execp_enabled;
int execp_pipe[2];
static timeout* execp_timeout;
char *execp_command;
int execp_has_icon;
int execp_cache_icon;
Imlib_Image execp_icon;
int execp_icon_w;
int execp_icon_h;
// interval in seconds
int execp_interval;

static char buf_tooltip[512];

// these are protected by a mutex, always use pthread_mutex_lock & pthread_mutex_unlock
// message content
static char *buf_txt;
// message icon
static char *icon_path;
// message index (used to order messages if they appear out of sync)
static int buf_index;
// the time the message was obtained
static time_t last_update_finished;
pthread_mutex_t buf_mutex = PTHREAD_MUTEX_INITIALIZER;

// this is the last requested message index
static int gui_buf_index;
static time_t last_update_timer_request;

struct execp_work {
	char *command;
	int index;
};

void timer_update_text(void* arg);
void notify_gui();
char* execp_get_tooltip(void* obj);

void default_execp()
{
	execp_enabled = 0;
	execp_timeout = NULL;
	execp_font_desc = NULL;
	gui_buf_index = 0;
	execp_command = NULL;
	execp_has_icon = 0;
	execp_cache_icon = 1;
	execp_icon = NULL;
	execp_interval = 30;
	last_update_timer_request = last_update_finished = 0;
	if (pipe(execp_pipe)) {
		fprintf(stderr, "Creating pipe failed.\n");
		execp_pipe[0] = -1;
		execp_pipe[1] = -1;
	} else {
		fcntl(execp_pipe[0], F_SETFL, O_NONBLOCK | fcntl(execp_pipe[0], F_GETFL));
		fcntl(execp_pipe[1], F_SETFL, O_NONBLOCK | fcntl(execp_pipe[1], F_GETFL));
	}
	pthread_mutex_lock(&buf_mutex);
	{
		buf_txt = strdup(" ");
		icon_path = NULL;
		buf_index = gui_buf_index;
	}
	pthread_mutex_unlock(&buf_mutex);
}

void cleanup_execp()
{
	if (execp_font_desc)
		pango_font_description_free(execp_font_desc);
	execp_font_desc = NULL;
	stop_timeout(execp_timeout);
	execp_timeout = NULL;
	pthread_mutex_lock(&buf_mutex);
	{
		free(buf_txt);
		buf_txt = NULL;
	}
	pthread_mutex_unlock(&buf_mutex);
	if (execp_pipe[0] > 0) {
		close(execp_pipe[1]);
		close(execp_pipe[0]);
		execp_pipe[0] = -1;
		execp_pipe[1] = -1;
	}
	free(execp_command);
	execp_command = NULL;
	if (execp_icon) {
		imlib_context_set_image(execp_icon);
		imlib_free_image();
		execp_icon = NULL;
	}
}

void init_execp()
{
	execp_timeout = add_timeout(10, execp_interval * 1000, timer_update_text, 0, &execp_timeout);
}

void init_execp_panel(void *p)
{
	Panel *panel =(Panel*)p;
	Execp *execp = &panel->execp;

	if (!execp_font_desc)
		execp_font_desc = pango_font_description_from_string(DEFAULT_FONT);
	if (!execp->area.bg)
		execp->area.bg = &g_array_index(backgrounds, Background, 0);
	execp->area.parent = p;
	execp->area.panel = p;
	execp->area._draw_foreground = draw_execp;
	execp->area.size_mode = SIZE_BY_CONTENT;
	execp->area._resize = resize_execp;
	execp->area._get_tooltip_text = execp_get_tooltip;

	execp->area.resize = 1;
	execp->area.on_screen = 1;
}

void reload_icon()
{
	if (execp_has_icon && icon_path) {
		if (execp_icon) {
			imlib_context_set_image(execp_icon);
			imlib_free_image();
		}
		if (execp_cache_icon) {
			execp_icon = imlib_load_image(icon_path);
		} else {
			execp_icon = imlib_load_image_without_cache(icon_path);
		}
		if (execp_icon) {
			imlib_context_set_image(execp_icon);
			int w, h;
			w = imlib_image_get_width();
			h = imlib_image_get_height();
			if (w && h) {
				if (execp_icon_w) {
					if (!execp_icon_h) {
						h = (int)(0.5 + h * execp_icon_w / (float)(w));
						w = execp_icon_w;
					} else {
						w = execp_icon_w;
						h = execp_icon_h;
					}
				} else {
					if (execp_icon_h) {
						w = (int)(0.5 + w * execp_icon_h / (float)(h));
						h = execp_icon_h;
					}
				}
				if (w < 1)
					w = 1;
				if (h < 1)
					h = 1;
			}
			if (w != imlib_image_get_width() || h != imlib_image_get_height()) {
				Imlib_Image icon_scaled = imlib_create_cropped_scaled_image(0, 0, imlib_image_get_width(), imlib_image_get_height(), w, h);
				imlib_context_set_image(execp_icon);
				imlib_free_image();
				execp_icon = icon_scaled;
			}
		}
	}
}

void draw_execp(void *obj, cairo_t *c)
{
	Execp *execp = obj;
	PangoLayout *layout;

	layout = pango_cairo_create_layout(c);

	if (execp_has_icon && execp_icon) {
		imlib_context_set_image(execp_icon);
		// Render icon
		if (server.real_transparency) {
			render_image(execp->area.pix,
						 execp->iconx,
						 execp->icony);
		} else {
			imlib_context_set_drawable(execp->area.pix);
			imlib_render_image_on_drawable(execp->iconx,
										   execp->icony);
		}
	}

	// draw layout
	pango_layout_set_font_description(layout, execp_font_desc);
	pango_layout_set_width(layout, execp->textw * PANGO_SCALE);
	pango_layout_set_alignment(layout, PANGO_ALIGN_LEFT);
	pthread_mutex_lock(&buf_mutex);
	{
		pango_layout_set_text(layout, buf_txt, strlen(buf_txt));
	}
	pthread_mutex_unlock(&buf_mutex);

	const int shadow_size = 3;
	const double shadow_edge_alpha = 0.05;
	int i, j;
	for (i = -shadow_size; i <= shadow_size; i++) {
		for (j = -shadow_size; j <= shadow_size; j++) {
			cairo_set_source_rgba(c, 0.0, 0.0, 0.0, 1.0 - (1.0 - shadow_edge_alpha) * sqrt((i*i + j*j)/(double)(shadow_size*shadow_size)));
			pango_cairo_update_layout(c, layout);
			cairo_move_to(c, execp->textx + i, execp->texty + j);
			pango_cairo_show_layout(c, layout);
		}
	}

	cairo_set_source_rgba(c, execp->font.color[0], execp->font.color[1], execp->font.color[2], execp->font.alpha);

	pango_cairo_update_layout(c, layout);
	cairo_move_to(c, execp->textx, execp->texty);
	pango_cairo_show_layout(c, layout);

	g_object_unref (layout);
}

int resize_execp(void *obj)
{
	Execp *execp = obj;
	Panel *panel = execp->area.panel;
	int horiz_padding = (panel_horizontal ? execp->area.paddingxlr : execp->area.paddingy);
	int vert_padding = (panel_horizontal ? execp->area.paddingy : execp->area.paddingxlr);
	int interior_padding =execp->area.paddingx;
	int txt_height_ink, txt_height, txt_width, ret = 0;
	int icon_w, icon_h;
	int text_next_line;

	execp->area.redraw = 1;

	if (execp_has_icon && icon_path) {
		reload_icon();
		if (execp_icon) {
			imlib_context_set_image(execp_icon);
			icon_w = imlib_image_get_width();
			icon_h = imlib_image_get_height();
		} else {
			icon_w = icon_h = 0;
		}
	} else {
		icon_w = icon_h = 0;
	}

	text_next_line = !panel_horizontal && icon_w > execp->area.width / 2;

	pthread_mutex_lock(&buf_mutex);
	{
		if (panel_horizontal) {
			get_text_size2(execp_font_desc, &txt_height_ink, &txt_height, &txt_width,
						   panel->area.width,
						   panel->area.height,
						   buf_txt, strlen(buf_txt));
		} else {
			get_text_size2(execp_font_desc, &txt_height_ink, &txt_height, &txt_width,
						   text_next_line
						   ? execp->area.width - 2*(horiz_padding + execp->area.bg->border.width)
						   : execp->area.width - icon_w - (icon_w ? interior_padding : 0) - 2*(horiz_padding + execp->area.bg->border.width),
						   panel->area.height,
						   buf_txt, strlen(buf_txt));
		}
	}
	pthread_mutex_unlock(&buf_mutex);

	if (panel_horizontal) {
		int new_size = txt_width;
		if (icon_w)
			new_size += interior_padding + icon_w;
		new_size += 2 * (horiz_padding + execp->area.bg->border.width);
		if (new_size > execp->area.width || new_size < (execp->area.width-6)) {
			// we try to limit the number of resize
			execp->area.width = new_size + 1;
			ret = 1;
		}
	} else {
		int new_size;
		if (!text_next_line) {
			new_size = txt_height + (2 * (vert_padding + execp->area.bg->border.width));
			if (new_size < icon_h + (2 * (vert_padding + execp->area.bg->border.width))) {
				new_size = icon_h + (2 * (vert_padding + execp->area.bg->border.width));
			}
		} else {
			new_size = icon_h + interior_padding + txt_height + (2 * (vert_padding + execp->area.bg->border.width));
		}
		if (new_size != execp->area.height) {
			// we try to limit the number of resize
			execp->area.height = new_size;
			ret = 1;
		}
	}
	execp->textw = txt_width;
	execp->texth = txt_height;
	if (execp->centered) {
		if (icon_w) {
			if (!text_next_line) {
				execp->icony = (execp->area.height - icon_h) / 2;
				execp->iconx = (execp->area.width - icon_w - interior_padding - txt_width) / 2;
				execp->texty = (execp->area.height - txt_height) / 2;
				execp->textx = execp->iconx + icon_w + interior_padding;
			} else {
				execp->icony = (execp->area.height - icon_h - interior_padding - txt_height) / 2;
				execp->iconx = (execp->area.width - icon_w) / 2;
				execp->texty = execp->icony + icon_h + interior_padding;
				execp->textx = (execp->area.width - txt_width) / 2;
			}
		} else {
			execp->texty = (execp->area.height - txt_height) / 2;
			execp->textx = (execp->area.width - txt_width) / 2;
		}
	} else {
		if (icon_w) {
			if (!text_next_line) {
				execp->icony = (execp->area.height - icon_h) / 2;
				execp->iconx = execp->area.bg->border.width + horiz_padding;
				execp->texty = (execp->area.height - txt_height) / 2;
				execp->textx = execp->iconx + icon_w + interior_padding;
			} else {
				execp->icony = (execp->area.height - icon_h - interior_padding - txt_height) / 2;
				execp->iconx = execp->area.bg->border.width + horiz_padding;
				execp->texty = execp->icony + icon_h + interior_padding;
				execp->textx = execp->iconx;
			}
		} else {
			execp->texty = (execp->area.height - txt_height) / 2;
			execp->textx = execp->area.bg->border.width + horiz_padding;
		}
	}

	return ret;
}

void execp_action(int button)
{
	if (button == 1) {
		if (execp_timeout)
			stop_timeout(execp_timeout);
		execp_timeout = add_timeout(10, execp_interval * 1000, timer_update_text, 0, &execp_timeout);
	}
}

const char* time_to_string(int seconds, char *buffer)
{
	if (seconds < 60) {
		sprintf(buffer, "%d second%s", seconds, seconds == 1 ? "" : "s");
	} else if (seconds < 60 * 60) {
		int m = seconds / 60;
		seconds = seconds % 60;
		int s = seconds;
		sprintf(buffer, "%d minute%s %d second%s", m, m == 1 ? "" : "s", s, s == 1 ? "" : "s");
	} else if (seconds < 24 * 60 * 60) {
		int h = seconds / (60 * 60);
		seconds = seconds % (60 * 60);
		int m = seconds / 60;
		seconds = seconds % 60;
		int s = seconds;
		sprintf(buffer, "%d hour%s %d minute%s %d second%s", h, h == 1 ? "" : "s", m, m == 1 ? "" : "s", s, s == 1 ? "" : "s");
	} else {
		int d = seconds / (24 * 60 * 60);
		seconds = seconds % (24 * 60 * 60);
		int h = seconds / (60 * 60);
		seconds = seconds % (60 * 60);
		int m = seconds / 60;
		seconds = seconds % 60;
		int s = seconds;
		sprintf(buffer, "%d day%s %d hour%s %d minute%s %d second%s", d, d == 1 ? "" : "s", h, h == 1 ? "" : "s", m, m == 1 ? "" : "s", s, s == 1 ? "" : "s");
	}
	return buffer;
}

char* execp_get_tooltip(void* obj)
{
	int current_index;
	time_t current_timestamp;
	time_t request_timestamp;
	time_t current_time;
	char tmp_buf1[256];
	char tmp_buf2[256];
	char tmp_buf3[256];

	pthread_mutex_lock(&buf_mutex);
	{
		current_index = buf_index;
		current_timestamp = last_update_finished;
	}
	pthread_mutex_unlock(&buf_mutex);
	request_timestamp = last_update_timer_request;
	current_time = time(NULL);

	if (current_index == gui_buf_index) {
		// message is in sync, we are not currently updating
		if (gui_buf_index > 0) {
			// we updated at least once
			sprintf(buf_tooltip, "Last update finished %s ago (took %s). Next update starting in %s.",
					time_to_string((int)(current_time - current_timestamp), tmp_buf1),
					time_to_string((int)(current_timestamp - request_timestamp), tmp_buf2),
					time_to_string((int)(execp_interval - (current_time - request_timestamp)), tmp_buf3));
		} else {
			// we never requested an update
			sprintf(buf_tooltip, "Never updated. No update scheduled.");
		}
	} else {
		// message is old, we are updating
		if (current_index > 0) {
			// we finished updating at least once
			sprintf(buf_tooltip, "Last update finished %s seconds ago. Update in progress (started %s seconds ago).",
					time_to_string((int)(current_time - current_timestamp), tmp_buf1),
					time_to_string((int)(current_time - request_timestamp), tmp_buf3));
		} else {
			// we never finished an update
			sprintf(buf_tooltip, "First update in progress (started %s seconds ago).",
					time_to_string((int)(current_time - request_timestamp), tmp_buf1));
		}
	}
	return strdup(buf_tooltip);
}

void* do_work(void *params);

void timer_update_text(void* arg)
{
	if (!execp_command)
		return;

	pthread_t worker;
	pthread_attr_t worker_attributes;
	struct execp_work *params = (struct execp_work *)malloc(sizeof(struct execp_work));
	if (!params) {
		pthread_mutex_lock(&buf_mutex);
		{
			free(buf_txt);
			buf_txt = strdup("Error: could not create thread");
		}
		pthread_mutex_unlock(&buf_mutex);
		notify_gui();
		return;
	}

	gui_buf_index++;
	params->command = execp_command;
	params->index = gui_buf_index;
	last_update_timer_request = time(NULL);
	if (pthread_attr_init(&worker_attributes) != 0) {
		free(params);
		pthread_mutex_lock(&buf_mutex);
		{
			free(buf_txt);
			buf_txt = strdup("Error: could not create thread");
		}
		pthread_mutex_unlock(&buf_mutex);
		notify_gui();
	}
	pthread_attr_setdetachstate(&worker_attributes, PTHREAD_CREATE_DETACHED);
	if (pthread_create(&worker, &worker_attributes, &do_work, params)) {
		free(params);
		pthread_mutex_lock(&buf_mutex);
		{
			free(buf_txt);
			buf_txt = strdup("Error: could not create thread");
		}
		pthread_mutex_unlock(&buf_mutex);
		notify_gui();
	}
}

#define OUTPUT_LIMIT 4096
char *get_command_output(char *cmd)
{
	int pipe_fd[2];
	pid_t child;

	if (pipe(pipe_fd)) {
		fprintf(stderr, "Creating pipe failed.\n");
		return NULL;
	}
	if ((child = fork()) == -1) {
		fprintf(stderr, "Fork failed.\n");
		return NULL;
	} else if (child == 0) {
		// We are in the child
		close(pipe_fd[0]);
		dup2(pipe_fd[1], 1); // 1 is stdout
		close(pipe_fd[1]);
		execl("/bin/sh", "/bin/sh", "-c", cmd, NULL);
		exit(0);
	}
	// We are in the parent
	close(pipe_fd[1]);

	char buffer[OUTPUT_LIMIT+1];
	ssize_t total_bytes_read = 0;
	while (1) {
		ssize_t bytes_read = read(pipe_fd[0], buffer+total_bytes_read, OUTPUT_LIMIT-total_bytes_read);
		if (bytes_read <= 0)
			break;
		total_bytes_read += bytes_read;
		if (total_bytes_read >= OUTPUT_LIMIT)
			break;
	}
	buffer[total_bytes_read] = '\0';
	// trim newlines from end
	while (total_bytes_read > 0) {
		if (buffer[total_bytes_read-1] == '\n') {
			buffer[total_bytes_read-1] = '\0';
			total_bytes_read--;
		} else {
			break;
		}
	}

	close(pipe_fd[0]);
	return strdup(buffer);
}

void* do_work(void *arg)
{
	struct execp_work *params = (struct execp_work *)(arg);

	char *txt = get_command_output(params->command);
	char *new_icon_path = NULL;

	if (!txt) {
		txt = strdup("Error: could not execute command");
	} else {
		if (execp_has_icon) {
			char *newtxt = strstr(txt, "\n");
			if (newtxt) {
				*newtxt = '\0';
				new_icon_path = strdup(txt);
				newtxt++;
				char *tmp = txt;
				txt = strdup(newtxt);
				free(tmp);
			} else {
				new_icon_path = txt;
				txt = strdup("");
			}
		}
	}

	pthread_mutex_lock(&buf_mutex);
	{
		if (buf_index < params->index) {
			buf_index = params->index;
			free(buf_txt);
			buf_txt = txt;
			free(icon_path);
			icon_path = new_icon_path;
			last_update_finished = time(NULL);
		} else {
			free(txt);
		}
	}
	pthread_mutex_unlock(&buf_mutex);

	notify_gui();

	free(params);

	return NULL;
}

void notify_gui()
{
	ssize_t wur = write(execp_pipe[1], "x", 1);
	(void) wur;
	fsync(execp_pipe[1]);
}
